Frontend Forever App
We have a mobile app for you to download and use. And you can unlock many features in the app.
Get it now
Intall Later
Run
HTML
CSS
Javascript
Output
Document
@charset "UTF-8"; @import url(https://fonts.googleapis.com/css?family=Nunito+Sans:300,400,600,700,800); *, :after, :before { box-sizing: border-box; padding: 0; margin: 0; } @use "sass:map"; $badgeExpandedWidth: 3.75em; $timings: ( "ease-in-out": cubic-bezier(0.65,0,0.35,1), "ease-in": cubic-bezier(0.33,0,0.67,0), "ease-out": cubic-bezier(0.33,1,0.67,1), ); * { border: 0; box-sizing: border-box; margin: 0; padding: 0; } :root { --hue: 223; --hue2: 133; --hue3: 3; --bg: hsl(var(--hue2),90%,70%); --fg: hsl(var(--hue),90%,10%); --primary: hsl(var(--hue),90%,50%); --trans-dur: 0.3s; --trans-timing: cubic-bezier(0.65,0,0.35,1); font-size: calc(20px + (60 - 20) * (100vw - 280px) / (3840 - 280)); } body, button { font: 1em/1.5 -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif; } body { background-color: var(--bg); color: var(--fg); display: flex; height: 100vh; transition: background-color var(--trans-dur), color var(--trans-dur); } .app { --dot-dur: 1s; background-color: hsl(0,0%,100%); border-radius: 1em; box-shadow: 0 0 0 0.333em hsla(0,0%,56%,0), 0 0.75em 1.5em hsla(var(--hue2),90%,30%,0.3); cursor: pointer; display: flex; margin: auto; outline: transparent; position: relative; width: 4em; height: 4em; transition: box-shadow calc(var(--trans-dur) / 2) var(--trans-timing); -webkit-appearance: none; appearance: none; -webkit-tap-highlight-color: transparent; &__badge { background-color: hsl(var(--hue3),90%,50%); border-radius: 0.75em; box-shadow: 0 0.28125em 0.5625em hsla(var(--hue3),90%,30%,0.5); overflow: hidden; padding: 0 0.375em; position: absolute; top: 0; right: 0; min-width: 1.5em; height: 1.5em; transform: translate(0.625em,-0.625em); transition: min-width var(--trans-dur) var(--trans-timing); &-count, &-text { color: hsl(0,0%,100%); font-weight: 300; transition: opacity var(--trans-dur) var(--trans-timing); } &-count { text-align: center; } &-text { opacity: 0; position: absolute; top: 0; left: 100%; width: max-content; } &:has(&-count:empty) { display: none; } } &:focus-visible { box-shadow: 0 0 0 0.333em hsla(0,0%,56%,1), 0 0.75em 1.5em hsla(var(--hue2),90%,30%,0.3); } &__icon { color: var(--primary); display: block; overflow: visible; pointer-events: none; margin: auto; width: 2.75em; height: 2.75em; } &:hover &, &:focus-visible & { &__badge { min-width: $badgeExpandedWidth; &-count { opacity: 0; } &-text { animation: marquee 5s linear infinite; opacity: 1; } } } &--animating &__icon { &-dot { animation: dot var(--dot-dur) map.get($timings,"ease-in-out"); &:nth-child(2) { animation-delay: calc(var(--dot-dur) * 0.05); } &:nth-child(3) { animation-delay: calc(var(--dot-dur) * 0.1); } } } } /* Animations */ @keyframes dot { from, 90%, to { animation-timing-function: map.get($timings,"ease-in-out"); transform: translateY(0); } 30% { animation-timing-function: map.get($timings,"ease-in"); transform: translateY(-32px); } 60% { animation-timing-function: map.get($timings,"ease-out"); transform: translateY(32px); } } @keyframes marquee { from { transform: translateX(0); } to { transform: translateX(calc(-100% - #{$badgeExpandedWidth})); } }
console.log("Event Fired") window.addEventListener("DOMContentLoaded",() => { const app = new MessageApp(".app"); }); class MessageApp { /** Element used for this component */ el: HTMLElement | null; /** Number of messages */ messageCount = 1; /** Preview of the first message */ messagePreview = "honey we need to talkā¦"; /** Animation is active */ isAnimating = false; /** Class used for the animation state */ animationClass = "app--animating"; /** Events to trigger the animation */ downEvents = ["focus","mouseover","touchstart"]; /** Events to stop the animation */ upEvents = ["blur","mouseout","touchend"]; /** * @param el CSS selector */ constructor(el: string) { this.el = document.querySelector(el); this.setupListeners(); this.displayBadge(); } /** Run the animation upon user interaction. */ addAnimation(): void { this.isAnimating = true; this.checkInteraction(); } /** Check if the user is still interacting before replaying the animation. */ checkInteraction(): void { this.el?.classList.remove(this.animationClass); if (this.isAnimating) { void this.el?.offsetWidth; this.el?.classList.add(this.animationClass); } } /** Show the notification count and first message. */ displayBadge(): void { const count = this.el?.querySelector("[data-count]") as HTMLElement; const preview = this.el?.querySelector("[data-preview]") as HTMLElement; if (count) { count.innerText = this.messageCount > 0 ? `${this.messageCount}` : ""; count.ariaLabel = `${this.messageCount} message(s)`; } if (preview) { preview.innerText = this.messagePreview; } } /** Stop the animation after its last iteration. */ removeAnimation(): void { this.isAnimating = false; } /** Set up the event listeners. */ setupListeners(): void { for (let downEvent of this.downEvents) { this.el?.addEventListener(downEvent,this.addAnimation.bind(this)); } for (let upEvent of this.upEvents) { this.el?.addEventListener(upEvent,this.removeAnimation.bind(this)); } // use the last dot for checking user interaction const dots = Array.from(this.el?.querySelectorAll("circle") || []); const lastDot = dots.pop(); lastDot?.addEventListener("animationend",this.checkInteraction.bind(this)); } }